var Arrows = {};
/**
 * A relay for communication between combinators and components. (Which could
 *    also be combinators) Avoids nasty memory leaks.
 *
 * @param combinator {Arrows.Combinator} The combinator
 * @param listeners {Object} Any listeners
 */
Arrows.Relay = function(combinator, listeners) {
	this.combinator = combinator || null;
	this.listeners = listeners || {};
	this.components = [];
};
/**
 * Add a component
 *
 * @param component {Arrows.Component} The component
 * @param listeners {Object} Any listeners
 */
Arrows.Relay.prototype.addComponent = function(component, listeners) {
	this.components.push({
		component: component,
		listeners: listeners
	});
};
/**
 * Remove a component
 *
 * @param component {Arrows.Component} The component
 */
Arrows.Relay.prototype.removeComponent = function(component) {
	var arr = [];
	for (var i=0; i<this.components.length; i++) {
		if (this.components[i].component !== component) {
			arr.push(this.components[i]);
		}
	}
	this.components = arr;
};
/**
 * Fire an event
 *
 * @param component {Arrows.Component} The component
 * @param eventName {String} The name of the event
 * @param args {Array} The arguments
 */
Arrows.Relay.prototype.fire = function(component, eventName, args) {
	var components = [];
	// If the component isn't the current combinator, it must be a child.
	//   So send along the message to the current combinator.
	if (component !== this.combinator) {
		components.push({
			component: this.combinator,
			listeners: this.listeners
		});
	} else {
		components = this.components;
	}

	for (var i=0; i<components.length; i++) {
		if (components[i] && components[i].listeners && components[i].listeners[eventName]) {
			components[i].listeners[eventName].call(components[i].component, component, args);
		}
	}
};
/**
 * Clear all the properties in the relay
 */
Arrows.Relay.prototype.clear = function() {
	for (var prop in this) {
		this[prop] = null;
	}
};
// Components
/**
 * A Component. Base class for all other components.
 */
Arrows.Component = function() {
	if (!(this instanceof Arrows.Component)) {
		var a = new Arrows.Component();
		Arrows.Component.apply(a, arguments);
		return a;
	}

	return this;
};
/**
 * Converts an object into a component
 *
 * @param item
 */
Arrows.Component.toComponent = function(item) {
	if (item instanceof Arrows.Component) {
		return item;
	}
	if (typeof item == "function") {
		return new Arrows.Function(item);
	}
	return new Arrows.Component();
}
/**
 * Starts the component
 *
 * @param relay {Arrows.Relay} The relay
 * @param args {Array} The arguments
 */
Arrows.Component.prototype.begin = function(relay, args) {
	// Don't do anything
	relay.fire(this, "complete", args);
};
/**
 * Run the Component
 */
Arrows.Component.prototype.run = function() {
	this.begin(new Arrows.Relay(), []);
	return this;
};
/**
 * An asynchronous component
 */
Arrows.AsynchronousComponent = function() {
	if (!(this instanceof Arrows.AsynchronousComponent)) {
		var a = new Arrows.AsynchronousComponent();
		Arrows.AsynchronousComponent.apply(a, arguments);
		return a;
	}

	Arrows.Component.apply(this, arguments);

	return this;
};
Arrows.AsynchronousComponent.prototype = new Arrows.Component;
/**
 * A Delay
 *
 * @param delay {Number} The number of milliseconds to delay.
 */
Arrows.Delay = function(delay) {
	if (!(this instanceof Arrows.Delay)) {
		var a = new Arrows.Delay();
		Arrows.Delay.apply(a, arguments);
		return a;
	}

	Arrows.AsynchronousComponent.apply(this, arguments);

	this._delay = delay;

	return this;
};
Arrows.Delay.prototype = new Arrows.AsynchronousComponent;
/**
 * Use set timeout to delay the execution of the next component.
 *
 * @param relay {Arrows.Relay} The relay
 * @param args {Array} The arguments
 */
Arrows.Delay.prototype.begin = function(relay, args) {
	var self = this;
	var timer;

	relay.addComponent(this, {
		"cancel": function(component) {
			if (component !== self) {
				window.clearTimeout(timer);
			}
		}
	});

	timer = window.setTimeout(function() {
		relay.fire(self, "progress", args);
		relay.fire(self, "complete", args);
	}, this._delay);

};
/**
 * A Function Component. Wraps a normal function call.
 */
Arrows.Function = function(f, context) {
	if (!(this instanceof Arrows.Function)) {
		var a = new Arrows.Function();
		Arrows.Function.apply(a, arguments);
		return a;
	}
	// Call the parent class' constructor
	Arrows.Component.apply(this, arguments);

	this._function = f;
	this._context = context || null;

	return this;
};
Arrows.Function.prototype = new Arrows.Component;
/**
 * Call the function
 *
 * @param relay {Arrows.Relay} The relay
 * @param args {Array} The arguments
 */
Arrows.Function.prototype.begin = function(relay, args) {
	relay.addComponent({
		component: this
	});

	args = [this._function.apply(this._context, args)];

	relay.fire(this, "complete", args);
};
// Combinators
/**
 * A combinator takes other components and does things with them.
 */
Arrows.Combinator = function() {
	if (!(this instanceof Arrows.Combinator)) {
		var a = new Arrows.Combinator;
		Arrows.Combinator.apply(a, arguments);
		return a;
	}

	Arrows.Component.apply(this, arguments);

	this.components = [];
	for (var i=0; i<arguments.length; i++) {
		this.components.push(Arrows.Component.toComponent(arguments[i]));
	}

	return this;
};
Arrows.Combinator.prototype = new Arrows.Component;
/**
 * Choose between two different options. The first to fire the progress event
 *   will continue. All the rest will be canceled.
 */
Arrows.Choose = function() {
	if (!(this instanceof Arrows.Choose)) {
		var a = new Arrows.Choose();
		Arrows.Choose.apply(a, arguments);
		return a;
	}

	Arrows.Combinator.apply(this, arguments);

	return this;
};
Arrows.Choose.prototype = new Arrows.Combinator;
/**
 * @param relay {Arrows.Relay} The relay
 * @param args {Array} The arguments
 */
Arrows.Choose.prototype.begin = function(relay, initialArgs) {
	var self = this;
	var newRelay = new Arrows.Relay(this, {
		"complete": function(component, args) {
			relay.removeComponent(self);
			relay.fire(self, "complete", args);
			newRelay.clear();
		},
		"progress": function(component, args) {
			// Fire the cancel event, passing this component as the first argument
			//   Components know not to cancel if they are equal to the first
			//   argument
			newRelay.fire(self, "cancel", [component]);
			// Inform the parent relay
			relay.fire(self, "progress", args);
		}
	});

	relay.addComponent(this, {
		"cancel": function(component, args) {
			newRelay.fire(self, "cancel", args);
		}
	});

	for (var i=0; i<this.components.length; i++) {
		this.components[i].begin(newRelay, initialArgs);
	}
};
/**
 * Do the various components in sequence
 */
Arrows.InSequence = function() {
	if (!(this instanceof Arrows.InSequence)) {
		var a = new Arrows.InSequence();
		Arrows.InSequence.apply(a, arguments);
		return a;
	}

	Arrows.Combinator.apply(this, arguments);

	return this;
};
Arrows.InSequence.prototype = new Arrows.Combinator;
/**
 * Start the sequence
 *
 * @param relay {Arrows.Relay} The relay
 * @param args {Array} The arguments
 */
Arrows.InSequence.prototype.begin = function(relay, initialArgs) {
	var self = this;
	var components = this.components;
	var next = 0;

	if (components.length) {
		var newRelay = new Arrows.Relay(this, {
			"complete": function(component, args) {
				args = args || [];

				if (next < components.length
					&& component !== Arrows.Break
					&& args[0] !== Arrows.Break) {
					components[next++].begin(newRelay, args);
				} else {
					relay.removeComponent(self);
					relay.fire(self, "complete", args);
					newRelay.clear();
				}
			},
			"progress": function(component, args) {
				// Inform the parent relay that we've made progress
				relay.fire(self, "progress", args);
			}
		});

		relay.addComponent(this, {
			"cancel": function(component, args) {
				newRelay.fire(self, "cancel", args);
			}
		});

		components[next++].begin(newRelay, initialArgs);
	}
};
/**
 * Repeat all the components in sequence.
 */
Arrows.Repeat = function() {
	if (!(this instanceof Arrows.Repeat)) {
		var a = new Arrows.Repeat();
		Arrows.Repeat.apply(a, arguments);
		return a;
	}

	Arrows.InSequence.apply(this, arguments);

	return this;
};
Arrows.Repeat.prototype = new Arrows.InSequence;
/**
 * Starts the repeat
 *
 * @param relay {Arrows.Relay} The relay
 * @param args {Array} The arguments
 */
Arrows.Repeat.prototype.begin = function(relay, initialArgs) {
	var self = this;
	var components = this.components;
	var next = 0;

	if (components.length) {
		var newRelay = new Arrows.Relay(this, {
			"complete": function(component, args) {
				args = args || [];
				var c = components[next++];

				// Cancel if this is a break.
				if (c === Arrows.Break || args[0] === Arrows.Break) {
					relay.removeComponent(self);
					relay.fire(self, "complete", args);
					newRelay.clear();
				// Start from the beginning if we've reached the end or
				//   we are supposed to restart
				} else if (c === Arrows.Restart || args[0] === Arrows.Restart || !c) {
					relay.removeComponent(self);
					self.begin(relay, initialArgs);
					newRelay.clear();
				} else {
					c.begin(newRelay, args);
				}
			},
			"progress": function(component, args) {
				// Inform the parent relay that we've made progress
				relay.fire(self, "progress", args);
			}
		});

		relay.addComponent(this, {
			"cancel": function(component, args) {
				newRelay.fire(self, "cancel", args);
			}
		});

		components[next++].begin(newRelay, initialArgs);
	}
};
/**
 * The restart arrow. Tells loops to start over.
 */
Arrows.Restart = new Arrows.Component();
/**
 * The break arrow. Tells loops to stop
 */
Arrows.Break = new Arrows.Component();
/**
 * Do the various components in parallel
 */
Arrows.InParallel = function() {
	if (!(this instanceof Arrows.InParallel)) {
		var a = new Arrows.InParallel();
		Arrows.InParallel.apply(a, arguments);
		return a;
	}

	Arrows.Combinator.apply(this, arguments);


	return this;
};
Arrows.InParallel.prototype = new Arrows.Combinator;
/**
 * Start all the components at the same time
 *
 * @param relay {Arrows.Relay} The relay
 * @param args {Array} The arguments
 */
Arrows.InParallel.prototype.begin = function(relay, initialArgs) {
	var self = this;
	var components = this.components;
	var done = 0;

	if (components.length) {
		var newRelay = new Arrows.Relay(this, {
			"complete": function(component, args) {
				args = args || [];
				done++;
				if (done >= components.length) {
					relay.removeComponent(self);
					relay.fire(self, "complete", args);
					newRelay.clear();
				}
			},
			"progress": function(component, args) {
				// Inform the parent relay that we've made progress
				relay.fire(self, "progress", args);
			}
		});

		relay.addComponent(this, {
			"cancel": function(component, args) {
				newRelay.fire(self, "cancel", args);
			}
		});

		for (var i=0; i<components.length; i++) {
			components[i].begin(newRelay, initialArgs);
		}
	}
};
